package group.pals.mailgate.media;

/*
 * Copyright © Jon Kristensen, 2008.
 * All rights reserved.
 * 
 * This is version 1.0 of this source code, made to work with JOrbis 1.x. The
 * last time this file was updated was the 15th of March, 2008.
 * 
 * Version history:
 * 
 * 1.0: Initial release.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *   * Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 * 
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 
 *   * Neither the name of jonkri.com nor the names of its contributors may be
 *     used to endorse or promote products derived from this software without
 *     specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/* Hai Bison's notes:
 * http://www.jcraft.com/jorbis/tutorial/Tutorial.html
 * 
 * I downloaded this file, then fix/ change some codes to use in my application:
 * - remove playing through URL.
 * - add playing through file.
 * - add start()/ stop() functions (they are not those names, just the meaning).
 * - add listener.
 * - change the name-style of constants.
 */
import com.jcraft.jogg.*;
import com.jcraft.jorbis.*;
import group.pals.threads.BaseThread;
import group.pals.threads.listeners.ThreadListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.IOException;
import java.nio.channels.ClosedByInterruptException;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;

/**
 * The <code>ExamplePlayer</code> thread class will simply download and play
 * OGG media. All you need to do is supply a valid URL as the first argument.
 *
 * @author Jon Kristensen
 * @version 1.0
 */
public class OggPlayer extends BaseThread {
    // If you wish to debug this source, please set the variable below to true.

    private final boolean DebugMode = false;

    /*
     * URLConnection and InputStream objects so that we can open a connection to
     * the media file.
     */
    private InputStream inputStream = null;

    /*
     * We need a buffer, it's size, a count to know how many bytes we have read
     * and an index to keep track of where we are. This is standard networking
     * stuff used with read().
     */
    byte[] buffer = null;
    int bufferSize = 2048;
    int count = 0;
    int index = 0;

    /*
     * JOgg and JOrbis require fields for the converted buffer. This is a buffer
     * that is modified in regards to the number of audio channels. Naturally,
     * it will also need a size.
     */
    byte[] convertedBuffer;
    int convertedBufferSize;
    // The source data line onto which data can be written.
    private SourceDataLine outputLine = null;
    // A three-dimensional an array with PCM information. 
    private float[][][] pcmInfo;
    // The index for the PCM information.
    private int[] pcmIndex;
    // Here are the four required JOgg objects...
    private final Packet JoggPacket;
    private final Page JoggPage;
    private final StreamState JoggStreamState;
    private final SyncState JoggSyncState;
    // ... followed by the four required JOrbis objects.
    private final DspState JorbisDspState;
    private final Block JorbisBlock;
    private final Comment JorbisComment;
    private final Info JorbisInfo;

    /**
     * Creates {@link OggPlayer} to play the source from a file name.
     * @param pathname the full path to file.
     * @throws FileNotFoundException if the file is not found.
     */
    public OggPlayer(String pathname) throws FileNotFoundException {
        this(new File(pathname));
    }

    /**
     * Creates {@link OggPlayer} to play the source from {@code file}.
     * @param file the {@code ogg} audio file.
     * @throws FileNotFoundException if {@code file} is not found.
     */
    public OggPlayer(File file) throws FileNotFoundException {
        this(new FileInputStream(file));
    }

    /**
     * Creates {@link OggPlayer} to play the source from {@link InputStream}.
     * @param inputStream the stream in {@code ogg} audio format.
     */
    public OggPlayer(InputStream inputStream) {
        super(String.format("%s [%s]", OggPlayer.class.getName(), new java.util.Date()));
        this.inputStream = inputStream;

        /*
         * Initialize other controls.
         */
        this.JoggPacket = new Packet();
        this.JoggPage = new Page();
        this.JoggStreamState = new StreamState();
        this.JoggSyncState = new SyncState();

        this.JorbisDspState = new DspState();
        this.JorbisBlock = new Block(this.JorbisDspState);
        this.JorbisComment = new Comment();
        this.JorbisInfo = new Info();
    }

    /**
     * This method is probably easiest understood by looking at the body.
     * However, it will - if no problems occur - call methods to initialize the
     * JOgg JOrbis libraries, read the header, initialize the sound system, read
     * the body of the stream and clean up.
     */
    @Override
    public void run() {
        // Check that we got an InputStream.
        if (inputStream == null) {
            System.err.println("We don't have an input stream and therefor "
                    + "cannot continue.");
            return;
        }

        // Initialize JOrbis.
        initializeJOrbis();

        /*
         * If we can read the header, we try to inialize the sound system. If we
         * could initialize the sound system, we try to read the body.
         */
        if (readHeader()) {
            if (initializeSound()) {
                readBody();
            }
        }

        // Afterwards, we clean up.
        cleanUp();

        //notify
        for (ThreadListener listener : Listeners) {
            listener.threadTerminated(this);
        }
    }

    /**
     * Initializes JOrbis. First, we initialize the <code>SyncState</code>
     * object. After that, we prepare the <code>SyncState</code> buffer. Then
     * we "initialize" our buffer, taking the data in <code>SyncState</code>.
     */
    private void initializeJOrbis() {
        waitWhileSuspending();
        if (isInterrupted()) {
            return;
        }

        debugOutput("Initializing JOrbis.");

        // Initialize SyncState
        JoggSyncState.init();

        // Prepare the to SyncState internal buffer
        JoggSyncState.buffer(bufferSize);

        /*
         * Fill the buffer with the data from SyncState's internal buffer. Note
         * how the size of this new buffer is different from bufferSize.
         */
        buffer = JoggSyncState.data;

        debugOutput("Done initializing JOrbis.");
    }

    /**
     * This method reads the header of the stream, which consists of three
     * packets.
     * 
     * @return true if the header was successfully read, false otherwise
     */
    private boolean readHeader() {
        waitWhileSuspending();
        if (isInterrupted()) {
            return false;
        }

        debugOutput("Starting to read the header.");

        /*
         * Variable used in loops below. While we need more data, we will
         * continue to read from the InputStream.
         */
        boolean needMoreData = true;

        /*
         * We will read the first three packets of the header. We start off by
         * defining packet = 1 and increment that value whenever we have
         * successfully read another packet.
         */
        int packet = 1;

        /*
         * While we need more data (which we do until we have read the three
         * header packets), this loop reads from the stream and has a big
         * <code>switch</code> statement which does what it's supposed to do in
         * regards to the current packet.
         */
        while (needMoreData) {
            waitWhileSuspending();
            if (isInterrupted()) {
                return false;
            }

            // Read from the InputStream.
            try {
                count = inputStream.read(buffer, index, bufferSize);
            } catch (ClosedByInterruptException cbie) {
                interrupt();
                notifyException(cbie);
            } catch (IOException exception) {
                System.err.println("Could not read from the input stream.");
                System.err.println(exception);
                notifyException(exception);
            }

            // We let SyncState know how many bytes we read.
            JoggSyncState.wrote(count);

            /*
             * We want to read the first three packets. For the first packet, we
             * need to initialize the StreamState object and a couple of other
             * things. For packet two and three, the procedure is the same: we
             * take out a page, and then we take out the packet.
             */
            switch (packet) {
                // The first packet.
                case 1: {
                    // We take out a page.
                    switch (JoggSyncState.pageout(JoggPage)) {
                        // If there is a hole in the data, we must exit.
                        case -1: {
                            System.err.println("There is a hole in the first "
                                    + "packet data.");
                            return false;
                        }

                        // If we need more data, we break to get it.
                        case 0: {
                            break;
                        }

                        /*
                         * We got where we wanted. We have successfully read the
                         * first packet, and we will now initialize and reset
                         * StreamState, and initialize the Info and Comment
                         * objects. Afterwards we will check that the page
                         * doesn't contain any errors, that the packet doesn't
                         * contain any errors and that it's Vorbis data.
                         */
                        case 1: {
                            // Initializes and resets StreamState.
                            JoggStreamState.init(JoggPage.serialno());
                            JoggStreamState.reset();

                            // Initializes the Info and Comment objects.
                            JorbisInfo.init();
                            JorbisComment.init();

                            // Check the page (serial number and stuff).
                            if (JoggStreamState.pagein(JoggPage) == -1) {
                                System.err.println("We got an error while "
                                        + "reading the first header page.");
                                return false;
                            }

                            /*
                             * Try to extract a packet. All other return values
                             * than "1" indicates there's something wrong.
                             */
                            if (JoggStreamState.packetout(JoggPacket) != 1) {
                                System.err.println("We got an error while "
                                        + "reading the first header packet.");
                                return false;
                            }

                            /*
                             * We give the packet to the Info object, so that it
                             * can extract the Comment-related information,
                             * among other things. If this fails, it's not
                             * Vorbis data.
                             */
                            if (JorbisInfo.synthesis_headerin(JorbisComment,
                                    JoggPacket) < 0) {
                                System.err.println("We got an error while "
                                        + "interpreting the first packet. "
                                        + "Apparantly, it's not Vorbis data.");
                                return false;
                            }

                            // We're done here, let's increment "packet".
                            packet++;
                            break;
                        }
                    }

                    /*
                     * Note how we are NOT breaking here if we have proceeded to
                     * the second packet. We don't want to read from the input
                     * stream again if it's not necessary.
                     */
                    if (packet == 1) {
                        break;
                    }
                }

                // The code for the second and third packets follow.
                case 2:
                case 3: {
                    // Try to get a new page again.
                    switch (JoggSyncState.pageout(JoggPage)) {
                        // If there is a hole in the data, we must exit.
                        case -1: {
                            System.err.println("There is a hole in the second "
                                    + "or third packet data.");
                            return false;
                        }

                        // If we need more data, we break to get it.
                        case 0: {
                            break;
                        }

                        /*
                         * Here is where we take the page, extract a packet and
                         * and (if everything goes well) give the information to
                         * the Info and Comment objects like we did above.
                         */
                        case 1: {
                            // Share the page with the StreamState object.
                            JoggStreamState.pagein(JoggPage);

                            /*
                             * Just like the switch(...packetout...) lines
                             * above.
                             */
                            switch (JoggStreamState.packetout(JoggPacket)) {
                                // If there is a hole in the data, we must exit.
                                case -1: {
                                    System.err.println("There is a hole in the first"
                                            + "packet data.");
                                    return false;
                                }

                                // If we need more data, we break to get it.
                                case 0: {
                                    break;
                                }

                                // We got a packet, let's process it.
                                case 1: {
                                    /*
                                     * Like above, we give the packet to the
                                     * Info and Comment objects.
                                     */
                                    JorbisInfo.synthesis_headerin(
                                            JorbisComment, JoggPacket);

                                    // Increment packet.
                                    packet++;

                                    if (packet == 4) {
                                        /*
                                         * There is no fourth packet, so we will
                                         * just end the loop here.
                                         */
                                        needMoreData = false;
                                    }

                                    break;
                                }
                            }

                            break;
                        }
                    }

                    break;
                }
            }

            // We get the new index and an updated buffer.
            index = JoggSyncState.buffer(bufferSize);
            buffer = JoggSyncState.data;

            /*
             * If we need more data but can't get it, the stream doesn't contain
             * enough information.
             */
            if (count == 0 && needMoreData) {
                System.err.println("Not enough header data was supplied.");
                return false;
            }
        }

        debugOutput("Finished reading the header.");

        return true;
    }

    /**
     * This method starts the sound system. It starts with initializing the
     * <code>DspState</code> object, after which it sets up the
     * <code>Block</code> object. Last but not least, it opens a line to the
     * source data line.
     * 
     * @return true if the sound system was successfully started, false
     *         otherwise
     */
    private boolean initializeSound() {
        waitWhileSuspending();
        if (isInterrupted()) {
            return false;
        }

        debugOutput("Initializing the sound system.");

        // This buffer is used by the decoding method.
        convertedBufferSize = bufferSize * 2;
        convertedBuffer = new byte[convertedBufferSize];

        // Initializes the DSP synthesis.
        JorbisDspState.synthesis_init(JorbisInfo);

        // Make the Block object aware of the DSP.
        JorbisBlock.init(JorbisDspState);

        // Wee need to know the channels and rate.
        int channels = JorbisInfo.channels;
        int rate = JorbisInfo.rate;

        // Creates an AudioFormat object and a DataLine.Info object.
        AudioFormat audioFormat = new AudioFormat((float) rate, 16, channels,
                true, false);
        DataLine.Info datalineInfo = new DataLine.Info(SourceDataLine.class,
                audioFormat, AudioSystem.NOT_SPECIFIED);

        // Check if the line is supported.
        if (!AudioSystem.isLineSupported(datalineInfo)) {
            System.err.println("Audio output line is not supported.");
            return false;
        }

        /*
         * Everything seems to be alright. Let's try to open a line with the
         * specified format and start the source data line.
         */
        try {
            outputLine = (SourceDataLine) AudioSystem.getLine(datalineInfo);
            outputLine.open(audioFormat);
        } catch (LineUnavailableException exception) {
            System.out.println("The audio output line could not be opened due "
                    + "to resource restrictions.");
            System.err.println(exception);
            notifyException(exception);
            return false;
        } catch (IllegalStateException exception) {
            System.out.println("The audio output line is already open.");
            System.err.println(exception);
            notifyException(exception);
            return false;
        } catch (SecurityException exception) {
            System.out.println("The audio output line could not be opened due "
                    + "to security restrictions.");
            System.err.println(exception);
            notifyException(exception);
            return false;
        }

        // Start it.
        outputLine.start();

        /*
         * We create the PCM variables. The index is an array with the same
         * length as the number of audio channels.
         */
        pcmInfo = new float[1][][];
        pcmIndex = new int[JorbisInfo.channels];

        debugOutput("Done initializing the sound system.");

        return true;
    }

    /**
     * This method reads the entire stream body. Whenever it extracts a packet,
     * it will decode it by calling <code>decodeCurrentPacket()</code>.
     */
    private void readBody() {
        waitWhileSuspending();
        if (isInterrupted()) {
            return;
        }

        debugOutput("Reading the body.");

        /*
         * Variable used in loops below, like in readHeader(). While we need
         * more data, we will continue to read from the InputStream.
         */
        boolean needMoreData = true;

        while (needMoreData) {
            waitWhileSuspending();
            if (isInterrupted()) {
                return;
            }

            switch (JoggSyncState.pageout(JoggPage)) {
                // If there is a hole in the data, we just proceed.
                case -1: {
                    debugOutput("There is a hole in the data. We proceed.");
                }

                // If we need more data, we break to get it.
                case 0: {
                    break;
                }

                // If we have successfully checked out a page, we continue.
                case 1: {
                    // Give the page to the StreamState object.
                    JoggStreamState.pagein(JoggPage);

                    // If granulepos() returns "0", we don't need more data.
                    if (JoggPage.granulepos() == 0) {
                        needMoreData = false;
                        break;
                    }

                    // Here is where we process the packets.
                    processPackets:
                    while (true) {
                        waitWhileSuspending();
                        if (isInterrupted()) {
                            return;
                        }

                        switch (JoggStreamState.packetout(JoggPacket)) {
                            // Is it a hole in the data?
                            case -1: {
                                debugOutput("There is a hole in the data, we "
                                        + "continue though.");
                            }

                            // If we need more data, we break to get it.
                            case 0: {
                                break processPackets;
                            }

                            /*
                             * If we have the data we need, we decode the
                             * packet.
                             */
                            case 1: {
                                decodeCurrentPacket();
                            }
                        }
                    }

                    /*
                     * If the page is the end-of-stream, we don't need more
                     * data.
                     */
                    if (JoggPage.eos() != 0) {
                        needMoreData = false;
                    }
                }
            }

            // If we need more data 
            if (needMoreData) {
                // We get the new index and an updated buffer.
                index = JoggSyncState.buffer(bufferSize);
                buffer = JoggSyncState.data;

                // Read from the InputStream.
                try {
                    count = inputStream.read(buffer, index, bufferSize);
                } catch (ClosedByInterruptException cbie) {
                    interrupt();
                    notifyException(cbie);
                } catch (IOException e) {
                    System.err.println(e);
                    notifyException(e);
                    return;
                } catch (Exception ex) {
                    interrupt();
                    notifyException(ex);
                    return;
                }

                // We let SyncState know how many bytes we read.
                JoggSyncState.wrote(count);

                // There's no more data in the stream.
                if (count == 0) {
                    needMoreData = false;
                }
            }
        }
        debugOutput("Done reading the body.");
    }

    /**
     * A clean-up method, called when everything is finished. Clears the
     * JOgg/JOrbis objects and closes the <code>InputStream</code>.
     */
    private void cleanUp() {
        debugOutput("Cleaning up.");

        // Clear the necessary JOgg/JOrbis objects.
        JoggStreamState.clear();
        JorbisBlock.clear();
        JorbisDspState.clear();
        JorbisInfo.clear();
        JoggSyncState.clear();
        if (outputLine != null) {
            outputLine.stop();
        }

        // Closes the stream.
        try {
            if (inputStream != null) {
                inputStream.close();
            }
        } catch (ClosedByInterruptException cbie) {
            interrupt();
            notifyException(cbie);
        } catch (IOException e) {
            notifyException(e);
        }

        debugOutput("Done cleaning up.");
    }

    /**
     *  Decodes the current packet and sends it to the audio output line.
     */
    private void decodeCurrentPacket() {
        int samples;

        // Check that the packet is a audio data packet etc.
        if (JorbisBlock.synthesis(JoggPacket) == 0) {
            // Give the block to the DspState object.
            JorbisDspState.synthesis_blockin(JorbisBlock);
        }

        // We need to know how many samples to process.
        int range;

        /*
         * Get the PCM information and count the samples. And while these
         * samples are more than zero...
         */
        while ((samples = JorbisDspState.synthesis_pcmout(pcmInfo, pcmIndex)) > 0) {
            waitWhileSuspending();
            if (isInterrupted()) {
                return;
            }

            // We need to know for how many samples we are going to process.
            if (samples < convertedBufferSize) {
                range = samples;
            } else {
                range = convertedBufferSize;
            }

            // For each channel...
            for (int i = 0; i < JorbisInfo.channels; i++) {
                waitWhileSuspending();
                if (isInterrupted()) {
                    return;
                }

                int sampleIndex = i * 2;

                // For every sample in our range...
                for (int j = 0; j < range; j++) {
                    waitWhileSuspending();
                    if (isInterrupted()) {
                        return;
                    }

                    /*
                     * Get the PCM value for the channel at the correct
                     * position.
                     */
                    int value = (int) (pcmInfo[0][i][pcmIndex[i] + j] * 32767);

                    /*
                     * We make sure our value doesn't exceed or falls below
                     * +-32767.
                     */
                    if (value > 32767) {
                        value = 32767;
                    }
                    if (value < -32768) {
                        value = -32768;
                    }

                    /*
                     * It the value is less than zero, we bitwise-or it with
                     * 32768 (which is 1000000000000000 = 10^15).
                     */
                    if (value < 0) {
                        value = value | 32768;
                    }

                    /*
                     * Take our value and split it into two, one with the last
                     * byte and one with the first byte.
                     */
                    convertedBuffer[sampleIndex] = (byte) (value);
                    convertedBuffer[sampleIndex + 1] = (byte) (value >>> 8);

                    /*
                     * Move the sample index forward by two (since that's how
                     * many values we get at once) times the number of channels.
                     */
                    sampleIndex += 2 * (JorbisInfo.channels);
                }
            }

            // Write the buffer to the audio output line.
            outputLine.write(convertedBuffer, 0, 2 * JorbisInfo.channels
                    * range);

            // Update the DspState object.
            JorbisDspState.synthesis_read(range);
        }
    }

    /**
     * This method is being called internally to output debug information
     * whenever that is wanted.
     * 
     * @param output the debug output information
     */
    private void debugOutput(String output) {
        if (DebugMode) {
            System.out.println("Debug: " + output);
        }
    }

    /*----------------------------------------------------------------------------
     * Event Notifiers
     */
}